본문으로 건너뛰기

Part 3: API 보안 설계

항목내용
문서명Part 3: API 보안 설계 (API Security Design)
제품명DTA Wide Sleep Management Platform
작성일2026-02-10
적용범위Part 3 (백엔드 API)

1. API 보안 개요​

목표: OWASP API Top 10 준수, 안전한 RESTful API 설계

보안 원칙:

  • 인증/인가 필수 (JWT Bearer Token)
  • 입력 검증 (DTO + class-validator)
  • Rate Limiting (Redis 기반)
  • 에러 메시지 최소 정보 노출
  • 관리자 엔드포인트 추가 보호

2. 인증 및 인가 (Authentication & Authorization)​

2.1 JWT 기반 인증​

AppToken 구조 (RS256 비대칭 서명):

// AppToken Payload (실제 구현 - app-token.guard.ts)
{
"appId": "app-uuid",
"deviceId": "device-uuid",
"jti": "token-unique-id",
"exp": 1707579000, // 30분 후 만료
"iat": 1707577200
}

인증 플로우:

토큰 무효화 정책:

비활성 감지 구현 현황:

항목비고
AccessToken TTL 30분 만료JWT exp 클레임 기반
RefreshToken TTL 14일 만료JWT exp 클레임 기반, 장기 미사용

2.2 역할 기반 인가 (RBAC)​

Decorator 사용:

@Controller('sleep')
@UseGuards(AppTokenGuard, FlexibleAuthGuard)
export class SleepController {
@Get('/logs')
@Roles('patient', 'clinician', 'admin')
async getSleepLogs(@CurrentUser() user: TokenPayload) {
// Patient: 본인 데이터만
// Clinician: 할당 환자만
// Admin: 모든 데이터
}

@Delete('/logs/:id')
@Roles('patient', 'admin')
async deleteSleepLog(@Param('id') id: string) {
// Patient: 본인 데이터만 삭제
// Admin: 모든 데이터 삭제 가능
}
}

2.3 앱 요청 무결성 및 진위성 검증​

현재 구현된 검증 레이어:

검증 레이어구현 상태참조 파일비고
AppToken (RS256) 앱 진위성 검증✅ 구현됨app-token.guard.ts, app-token.service.tsJWK 기반 RS256 서명 + DB 상태 확인
Device ID SHA-256 해시 검증✅ 구현됨device-identifier.service.ts요청 deviceIdHash vs 서버 계산값 비교
PubSub/Webhook GCP OIDC JWT 검증✅ 구현됨pubsub.guard.ts, webhook-validation.service.tsGCP Pub/Sub 요청 인증
HMAC 기반 요청 본문 서명 검증❌ 미구현-AppToken 이외 추가 서명 없음
응답 본문 HMAC/서명 추가❌ 미구현-응답에 서명 레이어 없음

3. 입력 검증 (Input Validation)​

3.1 DTO 기반 검증​

CreateSleepLogDto:

import { IsString, IsDateString, IsOptional, IsInt, Min, Max } from 'class-validator';

export class CreateSleepLogDto {
@IsDateString()
bedtime: string; // ISO 8601 형식

@IsDateString()
wakeTime: string;

@IsOptional()
@IsInt()
@Min(0)
@Max(480) // 최대 8시간
sol?: number; // Sleep Onset Latency (분)

@IsOptional()
@IsString()
@MaxLength(500)
notes?: string;
}

ValidationPipe 설정:

// main.ts
app.useGlobalPipes(
new ValidationPipe({
transform: true, // 자동 타입 변환
whitelist: true, // DTO에 없는 속성 제거
forbidNonWhitelisted: true, // 추가 속성 시 에러
exceptionFactory: (errors) => {
// 커스텀 에러 응답
return new BadRequestException({
code: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: errors.map(e => ({
property: e.property,
constraints: e.constraints
}))
});
}
})
);

3.2 SQL Injection 방지​

Prisma ORM 파라미터화:

// ✅ SAFE: Prisma 파라미터화된 쿼리
async findByUserId(userId: string): Promise<SleepLog[]> {
return this.prisma.sleepLog.findMany({
where: { userId: userId } // 자동 파라미터화
});
}

3.3 XSS 방지​

HTML 이스케이프:

import { sanitize } from 'class-sanitizer';

export class CreateNoteDto {
@IsString()
@MaxLength(1000)
@sanitize() // HTML 태그 제거
content: string;
}

// 또는 수동 이스케이프
function syntax(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}

4. Rate Limiting 및 Anti-Abuse​

4.1 Rate Limiting 정책​

엔드포인트 유형제한윈도우초과 시
인증 (로그인)5 req/분사용자당429 + 1분 대기
데이터 조회 (GET)100 req/분사용자당429 + 재시도 안내
데이터 생성 (POST)10 req/분사용자당429 + 재시도 안내
관리자 API60 req/분사용자당429 + 로그 기록
Public API1000 req/분IP당429 + CAPTCHA

4.2 Redis 기반 Rate Limiter​

구현 (NestJS Interceptor):

@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
constructor(private readonly redis: RedisService) {}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const userId = request.user?.userId || request.ip;
const key = `rate-limit:${request.path}:${userId}`;

const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, 60); // 1분 TTL
}

const limit = this.getLimit(request.path);
if (current > limit) {
throw new TooManyRequestsException(`Rate limit exceeded. Try again in 60 seconds.`);
}

return next.handle();
}
}

4.3 Brute Force 방어​

로그인 실패 제한:

async login(email: string, password: string) {
const key = `login-attempts:${email}`;
const attempts = await this.redis.get(key);

if (attempts && parseInt(attempts) >= 5) {
throw new TooManyRequestsException('Too many login attempts. Try again in 15 minutes.');
}

// 비밀번호 검증
const isValid = await this.verifyPassword(email, password);

if (!isValid) {
await this.redis.incr(key);
await this.redis.expire(key, 900); // 15분
throw new UnauthorizedException('Invalid credentials');
}

// 성공 시 카운터 리셋
await this.redis.del(key);
return this.generateToken(user);
}

5. 오류 메시지 정책 (Error Message Policy)​

5.1 안전한 에러 응답​

프로덕션 환경:

// ✅ SAFE: 최소 정보만 노출
{
"code": 2051,
"message": "INVALID_TOKEN",
"detail": "토큰이 유효하지 않습니다.",
"timestamp": "2026-02-10T12:00:00.000Z"
}

예외 필터 (Global Exception Filter):

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();

let status = 500;
let code = 'INTERNAL_SERVER_ERROR';
let detail = '서버 내부 오류가 발생했습니다.';

if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
code = exceptionResponse['code'] || exception.message;
detail = exceptionResponse['detail'] || exception.message;
}

// ❌ 스택 트레이스 절대 노출 금지
// ❌ 데이터베이스 연결 정보 노출 금지
// ❌ 파일 경로 노출 금지

response.status(status).json({
code,
message: code,
detail,
timestamp: new Date().toISOString()
});

// 내부 로깅 (Cloud Logging / LoggerService)
if (status >= 500) {
this.logger.error(exception);
}
}
}
항목구현 상태비고
에러 메시지 최소화 (스택 트레이스 미노출)✅ 구현됨GlobalExceptionFilter

5.2 HTTP 상태 코드 매핑​

시나리오HTTP 상태응답 코드메시지
인증 실패 (토큰 없음)401AUTH_REQUIRED"인증이 필요합니다."
인증 실패 (토큰 만료)401TOKEN_EXPIRED"토큰이 만료되었습니다."
권한 없음403FORBIDDEN"접근 권한이 없습니다."
리소스 없음404NOT_FOUND"리소스를 찾을 수 없습니다."
입력 검증 실패400VALIDATION_ERROR"입력값이 올바르지 않습니다."
Rate Limit 초과429RATE_LIMIT_EXCEEDED"요청 한도를 초과했습니다."
서버 오류500INTERNAL_ERROR"서버 오류가 발생했습니다."

6. 관리자 엔드포인트 보호 (Admin Endpoint Protection)​

6.1 추가 보안 계층​

관리자 전용 Guard:

@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;

// 1. Admin 역할 확인
if (!user.roles?.includes('admin')) {
return false;
}

// 2. IP 화이트리스트 확인 (선택적)
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
if (!this.isAllowedIp(clientIp)) {
this.logger.warn(`Admin access from unauthorized IP: ${clientIp}`);
return false;
}

// 3. 시간 기반 접근 제한 (선택적)
const hour = new Date().getUTCHours();
if (hour >= 0 && hour < 6) {
this.logger.warn(`Admin access during restricted hours: ${user.userId}`);
// 경고만, 차단하지 않음
}

return true;
}
}

6.2 민감 API 엔드포인트 목록​

엔드포인트역할추가 보안감사 로그
DELETE /v1/admin/users/:idAdminMFA 재확인✅
POST /v1/admin/users/:id/rolesAdminJira 승인✅
GET /v1/admin/audit-logsAdminIP 화이트리스트✅
POST /v1/admin/system/settingsAdminJira 승인✅
GET /v1/admin/users/:id/data-exportAdmin, CS데이터 익명화✅

7. API 보안 체크리스트​

#항목구현검증
1모든 엔드포인트 인증 필수 (public 제외)✅API Test
2역할 기반 인가 (RBAC)✅API Test
3DTO 입력 검증 (class-validator)✅API Test
4SQL Injection 방지 (Prisma ORM)✅SAST
5XSS 방지 (HTML 이스케이프)✅DAST
6CSRF 방지 (SameSite Cookie)N/AJWT Bearer Token 사용
7Rate Limiting (Redis)✅API Test
8안전한 에러 메시지✅API Test
9HTTPS 강제 (TLS 1.3)✅GCP SSL
10민감 데이터 로깅 금지✅로그 샘플 검증
11CORS 적절한 설정✅설정 파일 검토
12API 버저닝 (v1/)✅URL 패턴
13관리자 API 추가 보호✅Admin Guard
14감사 로그 (민감 행위)✅로그 샘플

8. OWASP API Top 10 준수​

#OWASP API Top 10구현 통제상태
1Broken Object Level AuthorizationuserId 검증, RBAC✅
2Broken AuthenticationJWT, Rate Limiting✅
3Broken Object Property Level AuthorizationDTO Whitelist✅
4Unrestricted Resource ConsumptionRate Limiting, 페이지네이션✅
5Broken Function Level AuthorizationRoles Guard✅
6Unrestricted Access to Sensitive Business FlowsRate Limiting, Block Access✅
7Server Side Request Forgery (SSRF)URL 검증, 화이트리스트✅
8Security Misconfiguration안전한 기본 설정, SAST✅
9Improper Inventory ManagementSwagger 문서, API 버저닝✅
10Unsafe Consumption of APIs외부 API TLS 검증, 타임아웃✅

9. Swagger API 문서 보안​

9.1 Swagger UI 접근 제한​

프로덕션 환경:

// main.ts
if (process.env.NODE_ENV !== 'production') {
// 스테이징/개발 환경만 Swagger 활성화
const config = new DocumentBuilder()
.setTitle('DTA Wide API')
.setVersion('1.0')
.addBearerAuth()
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('v1/docs', app, document);
} else {
// 프로덕션: Swagger 비활성화 또는 IP 화이트리스트
// [TODO: 프로덕션 Swagger 비활성화 확인 필요]
}

9.2 민감 정보 숨기기​

@ApiProperty({
example: 'user@example.com',
description: '사용자 이메일'
// ❌ 절대 포함 금지: 실제 이메일, 토큰, 비밀번호
})
email: string;

@ApiProperty({
example: '********', // 가려진 예시
writeOnly: true, // 응답에 포함 안 함
})
@Exclude() // class-transformer로 자동 제외
password: string;

증빙 및 참조(Artifacts)​

  1. API 보안 체크리스트 (본 문서 Section 7)
  2. DTO 검증 코드 - apps/dta-wide-api/src/app/*/dto/*.dto.ts
  3. Guards 구현 - guards/app-token.guard.ts, guards/flexible-auth.guard.ts, guards/webhook.guard.ts
  4. Rate Limiter 코드 - libs/core/redis/src/lib/rate-limit.interceptor.ts
  5. Exception Filter 코드 - apps/dta-wide-api/src/app/filters/global-exception.filter.ts
  6. OWASP ZAP DAST 보고서 - reports/api-dast-latest.pdf
  7. Swagger API 문서 - https://staging-api.dta-wide.com/v1/docs (스테이징)
  8. API 보안 테스트 결과 - test-results/api-security-tests.log
  9. Rate Limiting 테스트 - k6 스크립트 + 결과
  10. 감사 로그 샘플 (관리자 API) - logs/admin-api-calls.log
테스트 항목도구결과비고
SQL InjectionOWASP ZAP[TODO: 실행 필요]Prisma ORM (파라미터화)
XSS (Reflected)OWASP ZAP[TODO: 실행 필요]HTML 이스케이프
Broken AuthenticationBurp Suite[TODO: 실행 필요]AppToken RS256 + Rate Limit
IDOR (Insecure Direct Object Reference)Manual Test[TODO: 실행 필요]userId 검증
Rate Limitingk6 Load Test[TODO: 실행 필요]사용자당 100 req/min
CSRFOWASP ZAPN/AJWT Bearer Token (Cookie 미사용)
민감 정보 노출Manual Review[TODO: 실행 필요]에러 메시지 최소화